﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ChatClient
{
	public class HelloResponseEventArgs : EventArgs
	{
		public HelloResponseEventArgs(bool accepted, uint iteration, string commands)
		{
			Accepted = accepted;
			Iteration = iteration;
			Commands = commands;
		}

		public bool Accepted { get; set; }

		public uint Iteration { get; set; }

		public string Commands { get; set; }
	}

	public class MessagesReceivedEventArgs : EventArgs
	{
		private readonly ICollection<string> _messages = new List<string>();

		public MessagesReceivedEventArgs(uint startIteration)
		{
			StartIteration = startIteration;
		}

		public uint StartIteration { get; set; }

		public ICollection<string> Messages
		{
			get { return _messages; }
		}
	}

	public class ClientListReceivedEventArgs : EventArgs
	{
		private readonly ICollection<string> _users = new List<string>();

		public ICollection<string> Users
		{
			get { return _users; }
		}
	}

	internal class Client
	{
		private const string Ver = "1";
		private const int Timeout = 300;
		private const short Port = 12346;
		private const int ReceiveBufferSize = 1024*8;
		private const int ReceiveBlockSize = 1024;

		private readonly byte[] _receiveBuffer = new byte[ReceiveBufferSize];
		private Socket _clientSocket;
		private string _host;
		private uint _iteration;
		private string _name;
		private int _offset;
		private string _serverCommands;

		public Client()
		{
			for (int i = 0; i < _receiveBuffer.Length; ++i)
			{
				_receiveBuffer[i] = 66;
			}
		}

		public string Name
		{
			get { return _name; }
		}

		public string Host
		{
			get { return _host; }
		}

		public uint Iteration
		{
			get { return _iteration; }
			set { _iteration = value; }
		}

		public bool Connected
		{
			get
			{
				if (_clientSocket != null)
					return _clientSocket.Connected;
				return false;
			}
		}

		public event EventHandler<HelloResponseEventArgs> OnHelloResponse;
		public event EventHandler<MessagesReceivedEventArgs> OnMessagesReceived;
		public event EventHandler<ClientListReceivedEventArgs> OnClientListReceived;

		private void SocketReceiveData(IAsyncResult ar)
		{
			var currentClientSocket = (Socket) ar.AsyncState;
			int bytesRead = 0;
			try
			{
				bytesRead = currentClientSocket.EndReceive(ar);
			}
			catch
			{
				currentClientSocket.Close();
			}
			_offset += bytesRead;
			if (bytesRead > 0)
			{
				ProcessData();
			}
			if (currentClientSocket.Connected)
			{
				try
				{
					currentClientSocket.BeginReceive(_receiveBuffer, _offset, ReceiveBlockSize, new SocketFlags(), SocketReceiveData,
					                                 currentClientSocket);
				}
				catch
				{
					currentClientSocket.Close();
				}
			}
		}

		private void InitSocket()
		{
			_clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			_clientSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, Timeout);
		}

		public void Connect(string name, string host)
		{
			_host = host;
			_name = name;
			EndPoint remote = new IPEndPoint(IPAddress.Parse(host), Port);
			InitSocket();
			_clientSocket.Connect(remote);
			_clientSocket.BeginReceive(_receiveBuffer, 0, 1024, new SocketFlags(), SocketReceiveData, _clientSocket);
			_clientSocket.Send(Protocol.Hello(name, Ver));
		}

		public void Disconnect()
		{
			if (_clientSocket != null && _clientSocket.Connected)
			{
				_clientSocket.Send(Protocol.Bye());
				_clientSocket.Close();
			}
		}

		public void ProcessData()
		{
			if (_offset < 5)
			{
				return;
			}
			var length = (int) BitConverter.ToUInt32(_receiveBuffer, 0);
			if (length > _offset)
			{
				return;
			}
			string message = Encoding.ASCII.GetString(_receiveBuffer, 4, length);
			Array.Copy(_receiveBuffer, length + 4, _receiveBuffer, 0, _offset - length - 4);
			_offset = _offset - length - 4;

			if (message == "n")
			{
				if (OnHelloResponse != null)
				{
					OnHelloResponse(this, new HelloResponseEventArgs(false, 0, ""));
				}
			}
			else if (message.StartsWith("y"))
			{
				string[] strings = message.Split('\0');
				_iteration = Convert.ToUInt32(strings[1]);
				_serverCommands = strings[2];
				if (OnHelloResponse != null)
				{
					OnHelloResponse(this, new HelloResponseEventArgs(true, _iteration, _serverCommands));
				}
				GetHistory();
			}
			else if (message.StartsWith("list"))
			{
				if (OnClientListReceived != null)
				{
					string[] users = message.Split('\0');
					var args = new ClientListReceivedEventArgs();
					for (int i = 1; i < users.Length; ++i)
					{
						args.Users.Add(users[i]);
					}
					OnClientListReceived(this, args);
				}
			}
			else
			{
				string[] strings = message.Split('\0');
				_iteration = Convert.ToUInt32(strings[0]);
				var args = new MessagesReceivedEventArgs(_iteration);
				for (int i = 1; i < strings.Length; ++i)
				{
					if (strings[i] != "")
					{
						args.Messages.Add(strings[i]);
					}
				}
				OnMessagesReceived(this, args);
			}
		}

		public void SendMessage(string message)
		{
			_clientSocket.Send(Protocol.SendMessage(message));
		}

		public void GetHistory()
		{
			_clientSocket.Send(Protocol.GetN("0"));
		}

		public void GetClientList()
		{
			_clientSocket.Send(Protocol.GetClients());
		}

		public void GetMessages()
		{
			_clientSocket.Send(Protocol.GetN(Convert.ToString(_iteration)));
		}

		public static String[] RequestServers()
		{
			var _broadcastSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
			_broadcastSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, Timeout);
			_broadcastSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);

			EndPoint local = new IPEndPoint(IPAddress.Any, 0);
			_broadcastSocket.Bind(local);
			EndPoint target = new IPEndPoint(IPAddress.Broadcast, Port);
			_broadcastSocket.SendTo(new byte[] {0}, target);
			var servers = new List<string>();
			var receiveBuffer = new byte[1];
			EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0);
			while (true)
			{
				try
				{
					_broadcastSocket.ReceiveFrom(receiveBuffer, ref endPoint);
					servers.Add(((IPEndPoint) endPoint).Address.ToString());
				}
				catch (SocketException e)
				{
					break;
				}
			}
			return servers.ToArray();
		}
	}
}